package threading;

// 自己实现一个【定时器】！

import java.util.PriorityQueue;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;

// 写一个MyTask类来表示要执行的任务+执行时间
// 也就是说定时器的参数信息描述

// 因为优先级队列进行存储元素的时候需要按照一定的比较规则，
// 则此时应该在MyTask类中继承Comparable接口来重写compareTo方法规定比较方法，进而达到有序存储元素
class MyTask implements Comparable<MyTask> {
    // 要执行的任务信息:注意修饰的private 以及类型Runnable
    private Runnable runnable;
    // 说明时候执行任务（这是一个时间戳）
    private long time;

    // 给一个构造方法可以进行赋值操作
    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() +delay; // 一定要注意这里的赋值！！！
        // 不能直接将delay赋值给time，因为其实是在当前时间的delay（ms）之后执行！！
        // 所以此处是当前时间+delay 赋值，当达到该时间点后执行任务！！!
    }

    // 获取时间:为了之后比较看是否输出
    public Runnable getRunnable() {
        return runnable;
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time - o.time);
        // 是this - o，还是o-this
        // 写出来试一下，不行再进行调换
    }
}

// 自己写一个定时器类
class MyTimer {

    // 设置一个对象,用于wait、notify！！
    private Object lock = new Object();

    // 关键是 schedule方法！

    // 实现一个定时器多个任务！
    // 使用优先级队列
   // private PriorityQueue<MyTask> queue = new PriorityQueue<>();
    // 但是优先级队列不是线程安全的，而schedule方法是可以在多线程中调用的。
    // 所以：为了保证线程安全，就可以使用【阻塞队列BlockingQueue】来实现！！！
    private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    // 因为  BlockingQueue是一个接口，则创建实例的时候可以使用其实现的类的构造方法
    // 该类可以既保证优先级，又保证阻塞！

    public void schedule(Runnable runnable, long after) throws InterruptedException {
        // synchronized (lock) { // 如果把加锁/原子性加到这儿，会有什么问题？
        // 会出现死锁！！
        // 原因：两个加锁操作在互相等待：
        // 阻塞队列取队首元素，此时如果队列为空就会进入阻塞等待；
        // 那么啥时候会接触阻塞呢？ 就是在往队列中加了元素之后才会解除阻塞
        // 而此时去创建任务，往队列中添加元素是需要加锁的，但是此时的锁被扫描线程占用着还没释放
        // schedule 获取不到锁
        // 此时两者回想等待
        // 不是说synchronized就一定不会死锁！！

        MyTask myTask = new MyTask(runnable,after);
        // 创建MyTask对象之后其实就可以直接实现schedule方法了，因为具体的实现都在MyTask中了

        queue.put(myTask);  // 放入队列，此时任务已经被安排到优先级阻塞队列中了！

        // 任务被安排到优先级阻塞队列中了，接下来就需要从队列中取元素了：
        // 创建一个单独的扫描线程，让这个线程不停的来检查队首元素时间是否到了，如果时间到了则执行该任务。

        // 在这里插入新的任务，就要重新唤醒线程
        // 而唤醒线程的操作同样要加锁！！
        // 况且必须是同一对象

        // 这里的原子操作不可以扩大范围，否则会有问题！!!
        synchronized (lock) {
            lock.notify();
        }

    }

    // 构造方法
    public MyTimer() {
        // 线程！
        Thread t = new Thread(() -> {
            // 这个循环其实就是在【忙等】！！！
            // 使用wait来解决【忙等】问题
            // 使用sleep是不行的：因为如果有新的任务在sleep休眠的时间内，则该任务就无法在规定时间内执行完成。

            // 所以：应该在等待的过程中去释放CPU

            // 记录当前时间 为了之后更好的比较
            long curTime = System.currentTimeMillis();

            while (true) {
                // 取出队首元素
                try {
                    // 加锁范围扩大，保证了原子操作：take 到wait 不允许别的线程 “横插一脚”
                    synchronized (lock) {

                    // 由于阻塞队列无法阻塞式的取队首元素，所以：只能先取出任务再判定任务时间是否到了；
                    // 如果时间没到就再把任务放回去

                    MyTask myTask = queue.take();
                    // 判断是否到时间需要执行任务了
                    // 需要获取时间 来比较

                    // 假设take执行完成，然后切换到schedule线程
                    // schedule线程 新增一个任务， schedule线程执行完毕之后执行notify
                    // 然后回到该扫描线程
                    // 但是其实notify根本没有唤醒任何线程，因为这里take刚执行完就切换，还没执行wait
                    // 当切换回来后计算的等待时间其实还是最开始take的任务的等待时间，不是新增任务的等待时间
                    // 如果新增任务等待时间更短可能就会错过该任务的执行
                    // 主要问题其实就是：
                    // take操作和后续的判断、put、wait啥的不是原子操作
                    // 所以就进行加锁，需要保证take到wait之间的操作的原子性的，不能被其他线程插入！
                    // 避免出现notify的“虚晃一枪”

                    // 进行改进

                    // 所以这里就进行比较的更新
                    if(myTask.getTime() <= System.currentTimeMillis()) {

                        //if(myTask.getTime() <= System.currentTimeMillis()) {
                        // 如果设定时间 <= 当前时间 （也就是说：当前时间超过了设定时间就执行任务！！）
                        // 一定不要把比较的逻辑弄反了！ 是当前时间超过设定时间!!
                        myTask.getRunnable().run(); // 去执行该任务！

                    } else {
                        // 此时其实就是还没到执行时间：那就再把任务放回去
                        queue.put(myTask);

                        // 没到时间就进行等待 + 等待就需要加锁synchronized

                        // 保证原子性的操作：加锁的范围进行扩大！！
                        //synchronized (lock) {
                            lock.wait(myTask.getTime() - curTime);  // 设定一个等待时间
                        }
                        // 而插入新任务的时候就重新唤醒，避免错过其他任务
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}


public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        MyTimer myTimer = new MyTimer();
        // 使用用自己写的【定时器】类
        myTimer.schedule(new Runnable() { // 注意参数类型：Runnable的实例对象！！
            @Override
            public void run() {
                System.out.println("时间到喽！");
            }
        },3000);

        myTimer.schedule(new Runnable() { // 注意参数类型：Runnable的实例对象！！
            @Override
            public void run() {
                System.out.println("时间到嘻！");
            }
        },600);

        myTimer.schedule(new Runnable() { // 注意参数类型：Runnable的实例对象！！
            @Override
            public void run() {
                System.out.println("时间到ge！");
            }
        },6000);

        System.out.println("开始计时！");
    }
}

// 但其实还有一个问题：
// 如果是按照上述方式来等待就是非常不好的，因为这个等待过程中就只是做了等待查看是否到时间了，这就是纯纯浪费资源
// 也就是【忙等】
// 此时使用wait来解决【忙等】问题
